// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.alibaba.fastjson.JSON;
import com.eva.epc.common.util.CommonUtils;
import com.eva.epc.common.util.Platform;
import com.eva.framework.HttpController;
import com.eva.framework.Processor;
import com.eva.framework.Processor.User;
import com.eva.framework.dto.DataFromClient;
import com.eva.framework.dto.DataFromClient4Web;
import com.eva.framework.dto.DataFromServer;
import com.eva.framework.dto.LoginInfo2;
import com.eva.framework.dto.LogoutInfo;
import com.eva.framework.dto.SysProcessorConst;
import com.eva.framework.utils.EndsExceptionFactory;
import com.eva.framework.utils.LoggerFactory;
import com.google.gson.Gson;
import com.x52im.rainbowchat.common.dto.cnst.EncodeConf;
import com.x52im.rainbowchat.common.dto.cnst.MyProcessorConst;
import com.x52im.rainbowchat.common.dto.cnst.OperateLog;
import com.x52im.rainbowchat.http.logic.GroupChatProcessor;
import com.x52im.rainbowchat.http.logic.LogicProcessor2;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;
import com.x52im.rainbowchat.token.TokenManager;

/**
 * RainbowChat的服务端HTTP REST接口服务的MVC核心控制器实现类
 * （专用于浏览器端通过HTTP GET方式的JSONP数据交换格式）.
 * 
 * <p>
 * <b>说明：</b><br>
 * 本子类只需实现用户级的业务逻辑.
 * 
 * <p>
 * <b>调用：</b>HTTP的GET方式（因为GET方式是JSONP的唯一支持调用方式，无法使用POST）。<br>
 * <b>用途：</b>本类主要用于实现Web浏览器端的http rest接口调用，服务端返回数据格式为JSONP（意味着本
 * 类提供的http rest接口可以在浏览器端彻底解决跨域调用问题）。
 * 
 * <p>
 * <b>何为浏览器跨域问题？</b><br>
 * 为了安全考虑，浏览器厂商禁止网页端JS代码里调用当前域名、端口、子域名之外的链接（具体请自行百度）
 * ，这将导致您的浏览器端代码无法调用当前IM服务之外的任何链接。而JSONP数据交换格式是目前浏览器端
 * 用于解决跨域安全问题的最佳方案。有关浏览器跨域问题和JSONP的知识，请自行百度或查阅资料！
 * 
 * @author Jack Jiang(http://www.52im.net/space-uid-1.html)
 * @since 4.5
 */
public class MyControllerJSONP extends HttpController
{
	private final static long serialVersionUID = 1L;
	
	public final static String TAG = "HTTP.JSONP";
	
	/** 是否显示debug信息 */
	public static boolean DEBUG = true;

	public static Gson g = new Gson();
	
	private LogicProcessor2 logicProcessor = new LogicProcessor2();
	private GroupChatProcessor groupChatProcessor = new GroupChatProcessor();
	
	/**
	 * <p>
	 * 核心调度处理方法.<br>
	 * <br>
	 * 本方法默认会进行用户登陆认证处理.
	 * 
	 * @param dfc 封装了客户端发过来的相关数据（本方法中是必须的参数）
	 * @param req 客户端请求实例引用（把它传过来完全是未了考虑以后可能有其它作用，本方法中它不是必须的）
	 * @param res 客户端响应实例引用（把它传过来完全是未了考虑以后可能有其它作用，本方法中它不是必须的）
	 * @return 处理完返回的对象（要给客户端的）
	 * @throws Exception 本方法调用中出现任何异常时都会抛出，方法将中断执行
	 */
	// 本方法是父类中的抽象方法，子类中必须实现，从而实现自已的真正MVC控制器业务逻辑
	@Override protected Object dispatch(DataFromClient dfc, HttpServletRequest req,
			HttpServletResponse res) throws Exception
	{
		Object retObj = null;
		int processor_id = dfc.getProcessorId();
		// http接口编号
		String httpInterfaceNum = DataFromClient.constructHttpInterfaceNum(processor_id, dfc.getJobDispatchId(), dfc.getActionId());
		
		LoggerFactory.getLog().info("〖 ["+TAG+"][浏览器:"+dfc.getDevice()+"] 捕获REST【接口"+httpInterfaceNum+"】的请求，安全token="+dfc.getToken()+" 〗");
		
		Processor processor = null;
		
		// Rest框架需要首先对客户端发送过来的数据进行类型检查，防止非String类型的
		// 数据的恶意传递（因为浏览器传过来的肯定都是String）
		if((dfc.getNewData() != null && !(dfc.getNewData() instanceof String))
				|| (dfc.getOldData() != null && !(dfc.getOldData() instanceof String)))
		{
			String ds = "["+TAG+"]Rest框架要求客户端发过来的数据必须是JSON文本！而现在拿到的是：" +
					"newData="+(dfc.getNewData()!= null?dfc.getNewData().getClass().getName():"")
					+", oldData="+(dfc.getOldData()!= null?dfc.getOldData().getClass().getName():"");
			LoggerFactory.getLog().error(ds);
			throw new RuntimeException(ds);
		}
		// 【接口1013】浏览器端登陆接口（Web浏览器端可使用）
		else if(processor_id == MyProcessorConst.PROCESSOR_LOGIN_4ALL)
		{
			LoginInfo2 ai = JSON.parseObject((String)dfc.getNewData(), LoginInfo2.class);

			String uidOrmail = ai.getLoginName();
			String oprContent = ai.getDeviceInfo();

			//** 登陆验证
			LoggerFactory.getLog().debug("["+TAG+"]<登陆时：身份验证START> "+uidOrmail+"，接下来将进入真正的登陆验证流程.");
			// 登陆认证：返回null则表示认证实现，否则返回的是登陆用户的个人信息
			RosterElementEntity authedUser = MyControllerJSON.authrizationLogin(ai, req.getRemoteHost(), false);
			if(authedUser == null)
			{
				LoggerFactory.getLog().debug("["+TAG+"]<登陆时：身份验证END> "+uidOrmail+"的账号无效或登陆密码不正确(在其它手机修改过？).");
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_FALIURE, uidOrmail, oprContent);
				MyControllerJSON.addOperateLog(oprLog);
			}
			else
			{
				LoggerFactory.getLog().debug("["+TAG+"]<登陆时：身份验证END> "+uidOrmail+"身份认证成功！");
				// 记录操作日志
				OperateLog oprLog = new OperateLog(OperateLog.OPERATE_LOG_TYPE_LOGIN_SUCCESS, uidOrmail, oprContent);
				MyControllerJSON.addOperateLog(oprLog);
			}

			//** 返回值
			return JSON.toJSONString(authedUser);
		}
		// 【接口-2】浏览器端注销登陆接口（Web浏览器端可使用）
		else if(processor_id == MyProcessorConst.PROCESSSOR_LOGOUT)
		{
			LogoutInfo ao = JSON.parseObject((String)dfc.getNewData(), LogoutInfo.class);
			MyControllerJSON.authrizationLogout(ao, req.getRemoteHost());
			return null;
		}
		// 其它业务接口
		else
		{
			// 出于安全考虑，此token用于检查每一个http rest请求的合法性（token在用户登陆时由服务端生成并返回给客户端）
			TokenManager.verifyHttpToken(httpInterfaceNum, dfc.getToken());
			
			switch(processor_id)
			{
				// IM的基本业务逻辑接口处理器
				case MyProcessorConst.PROCESSOR_LOGIC:
					processor = logicProcessor;
					break;
				// 群组聊天相关管理接口的处理器
				case MyProcessorConst.PROCESSOR_GROUP_CHAT:
					processor = groupChatProcessor;
					break;
				//找不到处理器
				default:
					throw EndsExceptionFactory.INVALID_PROCESSOR_ID(processor_id);
			}

			String basePath = req.getScheme()+"://"+req.getServerName()+":"+req.getServerPort()+req.getContextPath() + "/";
//			System.out.println("[HTTP]basePath = " +  basePath);
			// 借用一个User对象，仅仅是用来存放Ip地址而已
			User u = new User();
			u.setIp(req.getRemoteHost());
			if (processor != null)
			{
				if(processor instanceof LogicProcessor2)
				{
					retObj = processor.process(dfc.getJobDispatchId()
							, dfc.getActionId()
							, dfc.getNewData()
							, new Object[]{basePath, dfc.getOldData()}
					, u);
				}
				else
					retObj = processor.process(dfc.getJobDispatchId()
							, dfc.getActionId(), dfc.getNewData()
							, dfc.getOldData(), u);
				
				// 返回值类型检查
				if(retObj != null && !(retObj instanceof String))
				{
					String ds = "["+TAG+"]Rest框架中processor处理完成" +
							"的返回值必须是JSONP文本哦！(retObj="+retObj.getClass().getName()+")";
					LoggerFactory.getLog().error(ds);
					throw new RuntimeException(ds);
				}
			}
			
			return retObj;
		}
	}
	
	/**
	 * 核心处理方法.<br>
	 * <p>
	 * 本方法会确保处理完成后写回一个DataFromServer对象（客户端通过它可知是否处理成功等信息）给客端，如果处理成功则
	 * DataFromServer对象中的sucess字值会被设置成true且returnValue会设置成处理完成的返回的对象，否则sucess将被设置成
	 * false且returnValue里存放的将是错误消息文本（该文本不能保证一定不是null）.
	 * </p>
	 * <pre>
	 * 	注：子类的处理实现推荐overload方法processImpl(DataFromClient, HttpServletRequest, HttpServletResponse)
	 * ，一般情况下不推荐重写本方法.
	 * </pre>
	 * 
	 * @param req HttpServletRequest 客户端请求HttpServletRequest对象
	 * @param res HttpServletResponse 客户端响应HttpServletResponse对象
	 * @see #dispatch(DataFromClient, HttpServletRequest, HttpServletResponse)
	 * @see DataFromServer
	 * @throws Exception 本方法调用中出现任何异常时都会抛出
	 */
	@Override
	// 重写本父类方法的目的，是实现向web浏览器端返回JSONP数据格式
	protected void process(HttpServletRequest req,HttpServletResponse res) throws Exception
	{
		//服务端是否要写回数据给客户端，此字段目前为保留字段，未有具体意义
		boolean needRecieve = true;
		
		String retFormat = DataFromClient4Web.RET_FORMAT_JSON;
		String retCallback = null;
		
		//处理完成后将要返回给客端的数据对象（通过它可知是否处理成功等信息）
		DataFromServer objFromServer = null;//new PojoFromServer();
		try
		{
			if(DEBUG)
			{
				LoggerFactory.getLog().debug("["+TAG+"] - >>S 客户端Cookie="+req.getRequestedSessionId()
					+" ip="+req.getRemoteHost()+":"+req.getRemotePort());
			}
			
			//第1/3步：读取Web浏览器端发过来的数据
			DataFromClient4Web dataFromClient = recieveFromClient(req);
			retFormat = dataFromClient.getFormat();
			retCallback = dataFromClient.getJsoncallback();
			
//			//* 服务端是否要写回数据给客户端
//			needRecieve = dataFromClient.isDoInput();
			
			//第2/3步：提交处理器进行业务处理
			Object processedData = dispatch(dataFromClient,req,res);//processImpl(dataFromClient, req, res);
			if(objFromServer == null)
				objFromServer = new DataFromServer();
			//处理成功了
			objFromServer.setSuccess(true);
			objFromServer.setReturnValue(processedData);
		}
		catch (EndsExceptionFactory.EInvalidTokenException e0)
		{
			objFromServer = new DataFromServer();
			objFromServer.setSuccess(false);
			// 针对无法的token单独设置返回码，便于客户端鉴别
			objFromServer.setCode(DataFromServer.CODE_INVALID_TOKEN);
			// 邮js改成只返回简单错误信息，而非完整Exception对象，目的是在跨平台实现时反射成JSON时不会StackOverflowError
			objFromServer.setReturnValue(CommonUtils.getExceptionStackTraceForPlainText(e0));//e);
			LoggerFactory.getLog().warn("["+TAG+"] - "+e0.getMessage(), e0);
			throw e0;
		}
		catch (Exception e)
		{
			objFromServer = new DataFromServer();
			objFromServer.setSuccess(false);
//			objFromServer.setReturnValue("在方法process(..)中出错,原因是:"+e.getMessage());
			// 邮js改成只返回简单错误信息，而非完整Exception对象，目的是在跨平台实现时反射成JSON时不会StackOverflowError
			objFromServer.setReturnValue(CommonUtils.getExceptionStackTraceForPlainText(e));//e);
			LoggerFactory.getLog().warn("["+TAG+"] - "+e.getMessage(), e);
			throw e;
		}
		finally
		{
			//* 服务端需要写回数据给客户端
			if(needRecieve)
			{
				//第3/3步：将处理完成的数据对象发回给客户端
				sendToClient(req, res,objFromServer, retFormat, retCallback);
			}
			
			if(DEBUG)
			{
				LoggerFactory.getLog().debug("["+TAG+"] - >>E 处理完成,客户端"+ " ip="+req.getRemoteHost()+":"+req.getRemotePort()
						+" 的请处理成功了?"+objFromServer.isSuccess()+(needRecieve?
								" - ret="+objFromServer.getReturnValue().getClass()//objFromServer.getReturnValue() 直接输出返回值用于早期MVC框架调试时，生产环境下将大幅影响系统性能
							:" - [doInput=false,无需返回值给客户端]"));
			}
		}
	}
	
	/**
	 * 将数据对象发回给Web浏览器端.
	 * 
	 * @param res 客户端响应HttpServletResponse对象
	 * @param objFromServer 要发送的可系列化的数据对象
	 * @throws Exception 如果在操作OutputStream时出现异常
	 */
	protected void sendToClient(HttpServletRequest req, HttpServletResponse res
			,DataFromServer objFromServer, String retFormat, String retCallBack) throws IOException
	{
		res.setContentType("text/html;charset="+EncodeConf.ENCODE_TO_CLIENT);  
		
		// 返回结果
		String retStr = null;
		try{
			// 如果返回结果是JSONP
			if(DataFromClient4Web.RET_FORMAT_JSONP.equals(retFormat))
				retStr = retCallBack+"("+g.toJson(objFromServer)+");";
			// 否则返回的是JSON
			else
				retStr = g.toJson(objFromServer);
			
			res.getOutputStream().write(retStr.getBytes(EncodeConf.ENCODE_TO_CLIENT));  
			
			if(DEBUG)
			{
				LoggerFactory.getLog().info("["+TAG+"]【<== 处理完成-返回JSONP结果】:"+retStr);
			}
		}
		catch (Exception e2){
			LoggerFactory.getLog().warn("["+TAG+"] - "+e2.getMessage(), e2);
		}

//		if(DEBUG)
//		{
//			LoggerFactory.getLog().debug("["+TAG+"] - >>已处理完成客户端ip="+req.getRemoteHost()
//					+":"+req.getRemotePort()+"的GET请求，结果="+retStr);
//		}
	}
	
	/**
	 * 读取PC客户端传过来的数据对象。
	 * 
	 * @param req 客户端请求HttpServletRequest对象
	 * @return 返回读取出来的客户端数据对象
	 * @throws IOException 如果req.getInputStream()出现异常时抛出
	 * @throws ClassNotFoundException 如果is.readObject()出现异常时抛出
	 */
	@Override
	// 重写本父类方法的目的是读取Web浏览器端通过GET方式发过来的数据（因为RainbowChat-Web
	// 的浏览器端为了解决跨域问题，只能使用JONP的方式，而JSONP又只能用http GET而不能http POST）
	protected DataFromClient4Web recieveFromClient(HttpServletRequest req)
			throws IOException, ClassNotFoundException
	{
		int processorId= CommonUtils.getIntValue(req.getParameter("processorId"), -9999999);
		int jobDispatchId= CommonUtils.getIntValue(req.getParameter("jobDispatchId"), -9999999);
		int actionId= CommonUtils.getIntValue(req.getParameter("actionId"), -9999999);
		String token = req.getParameter("token");
		
		String newData = MyControllerJSON.SQLSave(getStringFromURL(req.getParameter("newData")));
		String oldData = MyControllerJSON.SQLSave(getStringFromURL(req.getParameter("oldData")));
		
		String retFormat = req.getParameter("format");
		// 本参数只在format=jsonp时有意义，表示jsonp对应的客户端JS回调函数
		// 补充：为了兼容jQuery的跨域回调，参数名就定为jsoncallback了
		String retCallBack = req.getParameter("jsoncallback");
		
		if(DEBUG)
		{
			LoggerFactory.getLog().info("["+TAG+"]【==> 收到接口请求-数据原型】: processor_id="+processorId
					+", job_id="+jobDispatchId+", action_id="+actionId+", token="+token
					+", new_data="+newData+", old_data="+oldData
					+", format="+retFormat+", jsoncallback="+retCallBack);
		}
		
		DataFromClient4Web dfc = DataFromClient4Web.n();
		dfc.setProcessorId(processorId);
		dfc.setJobDispatchId(jobDispatchId);
		dfc.setActionId(actionId);
		dfc.setToken(token);
		dfc.setNewData(newData);
		dfc.setOldData(oldData);
		dfc.setDevice(2);
		dfc.setFormat(retFormat);
		dfc.setJsoncallback(retCallBack);

		return dfc;
	}
	
	/**
	 * 解决http GET方式发过来的参数中的中文乱码问题。
	 * 参考文章：https://blog.csdn.net/chenssy/article/details/42963595
	 * 
	 * @param strFromURL
	 * @return
	 */
	private static String getStringFromURL(String strFromURL)
	{
		try 
		{
			if(strFromURL != null)
			{
				// 注意：windows环境下（无论是开发，还是生产部署）运行时，请勿设
				//      置tomcat中server.xml的“URIEncoding="UTF-8"”，否则请去掉本行针对windows的特殊处理，
				//      详见文章：https://www.jb51.net/article/80181.htm
//				if(Platform.isWindows())
//					return new String(strFromURL.getBytes("ISO-8859-1"),"UTF-8");
//				// Liux等类Unix操作系统下，解决乱码，请在tomcat中server.xml的Connector中增加“URIEncoding="UTF-8"”配置属性即可
//				else
					return strFromURL;
			}
		} 
		catch (Exception e) 
		{
			LoggerFactory.getLog().error(e.getMessage(), e);
		}
		return strFromURL;
	}
	
	/**
	 * 强制保护浏览器端JSON.parese()可能存在多次JSON嵌套时，因特殊字符未被尽职转义而解析出错的问题。
	 * 
	 * @param originalStr
	 * @return
	 */
	public static String JSONSave(String originalStr)
	{
		if(originalStr != null)
		{
			// 浏览器端的JSON.parese()函数对多层嵌套JSON中有些特殊字符没有办法正确解析，
			// 因而以下代码为了保证浏览器端的正确性，强制进行个别特殊字符的转义 - 启用于20190412日
			return originalStr
//					.replaceAll("\"", "&quot;")
//					.replaceAll("\'", "&#39;")
//					.replaceAll("\\", "\\\\")
					.replaceAll("\n", " ")//\\n
					.replaceAll("\r", " ")//\\r
					.replaceAll("\t", " ")//\\t
//					.replaceAll("\\n", " ")
//					.replaceAll("\\r", " ")
//					.replaceAll("\\t", " ")
					;
		}
		
		return originalStr;
	}
}
